再谈 OpenSSL 的跨平台

2,583 阅读5分钟
原文链接: mp.weixin.qq.com

出于项目需要,再一次的拿起 OpenSSL,原本以为按照老方法可以方便的移植,但是在 Mac 最新版本 + NDK 最新版本 + iOS SDK 10.3 + OpenSSL 1.1.0e 的环境下,似乎老的移植方法已经不那么好用了。

微店大神区长之前给出了移植的方案(点击文末阅读原文可查看),很可惜的,这个方案并不完美工作,还是有一些工作需要自己来做。

移植到 Android

直接上脚本,其中需要注意的是,克隆出来的 toolchain 我是按架构存放了不同的版本,因为编译出来的产物,还可能被其他的组件使用,比如说 curl 等。

#!/bin/sh

if [ ! -f "openssl-1.1.0e.tar.gz" ]; then
   wget https://www.openssl.org/source/openssl-1.1.0e.tar.gz
fi

if [ ! -d "openssl-1.1.0e" ]; then
   tar zxf openssl-1.1.0e.tar.gz
fi

# env
if [ -d "out/openssl" ]; then
   rm -fr "out/openssl"
fi

mkdir "out"
mkdir "out/openssl"

_compile() {
   SURFIX=$1
   TOOL=$2
   ARCH_FLAGS=$3
   ARCH_LINK=$4
   CFGNAME=$5
   ARCH=$6

   if [ ! -d "out/openssl/${SURFIX}" ]; then
       mkdir "out/openssl/${SURFIX}"
   fi

   if [ ! -d "toolchain_${SURFIX}" ]; then
       $ANDROID_NDK/build/tools/make-standalone-toolchain.sh --arch=${ARCH} --install-dir=./toolchain_${SURFIX}
   fi
   export ANDROID_HOME=`pwd`
   export TOOLCHAIN=$ANDROID_HOME/toolchain_${SURFIX}
   export CROSS_SYSROOT=$TOOLCHAIN/sysroot
   export PATH=$TOOLCHAIN/bin:$PATH
   export CC=$TOOLCHAIN/bin/${TOOL}-gcc
   export CXX=$TOOLCHAIN/bin/${TOOL}-g++
   export LINK=${CXX}
   export LD=$TOOLCHAIN/bin/${TOOL}-ld
   export AR=$TOOLCHAIN/bin/${TOOL}-ar
   export RANLIB=$TOOLCHAIN/bin/${TOOL}-ranlib
   export STRIP=$TOOLCHAIN/bin/${TOOL}-strip
   export ARCH_FLAGS=$ARCH_FLAGS
   export ARCH_LINK=$ARCH_LINK
   export CFLAGS="${ARCH_FLAGS} -fpic -ffunction-sections -funwind-tables -fstack-protector -fno-strict-aliasing -finline-limit=64"
   export CXXFLAGS="${CFLAGS} -frtti -fexceptions"
   export LDFLAGS="${ARCH_LINK}"


   cd openssl-1.1.0e/
   ./Configure ${CFGNAME} --prefix=$TOOLCHAIN/sysroot/usr/local --with-zlib-include=$TOOLCHAIN/sysroot/usr/include --with-zlib-lib=$TOOLCHAIN/sysroot/usr/lib zlib no-asm no-shared no-unit-test
   make clean
   make -j4
   make install
   cd ..
   mv openssl-1.1.0e/libssl.a out/openssl/${SURFIX}/
   mv openssl-1.1.0e/libcrypto.a out/openssl/${SURFIX}/
}


# arm
_compile "armeabi" "arm-linux-androideabi" "-mthumb" "" "android" "arm"

# armv7
_compile "armeabi-v7a" "arm-linux-androideabi" "-march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16" "-march=armv7-a -Wl,--fix-cortex-a8" "android-armeabi" "arm"

# x86
_compile "x86" "i686-linux-android" "-march=i686 -msse3 -mstackrealign -mfpmath=sse" "" "android-x86" "x86"

echo "done"

区长还提供了更多架构的编译方案,如下:

# arm64v8
_compile "arm64-v8a" "aarch64-linux-android" "" "" "android64-aarch64" "arm64"

# x86_64
_compile "x86_64" "x86_64-linux-android" "-march=x86-64 -m64 -msse4.2 -mpopcnt  -mtune=intel" "" "android64" "x86_64"

# mips
_compile "mips" "mipsel-linux-android" "" "" "android-mips" "mips"

# mips64
_compile "mips64" "mips64el-linux-android" "" "" "linux64-mips64" "mips64"

移植到 iOS

iOS 平台比 Android 要折腾一些,先前我从网上找了很多移植的脚本,都不管用,在最新的环境下均无法编译通过,无奈之下只好自己写。

#!/bin/sh

if [ ! -f "openssl-1.1.0e.tar.gz" ]; then
   wget https://www.openssl.org/source/openssl-1.1.0e.tar.gz
fi

if [ ! -d "openssl-1.1.0e" ]; then
   tar zxf openssl-1.1.0e.tar.gz
fi

if [ -d "out/openssl" ]; then
   rm -fr "out/openssl"
fi

mkdir "out"
mkdir "out/openssl"
mkdir "out/openssl/mac"
mkdir "out/openssl/ios"

SDK_VERSION=10.3
DEVELOPER=`xcode-select -print-path`

_compileMac() {
 ARCH=$1
 TARGET=$2
 cd openssl-1.1.0e
 ./Configure ${TARGET}
 make clean
 make -j4
 mv libssl.a libssl-${ARCH}.a
   mv libcrypto.a libcrypto-${ARCH}.a
 cd ..
}

_compileMac "i386" "darwin-i386-cc"
_compileMac "x86_64" "darwin64-x86_64-cc"

lipo -create openssl-1.1.0e/libssl-i386.a openssl-1.1.0e/libssl-x86_64.a -output out/openssl/mac/libssl.a
lipo -create openssl-1.1.0e/libcrypto-i386.a openssl-1.1.0e/libcrypto-x86_64.a -output out/openssl/mac/libcrypto.a

_compileIOS() {
 ARCH=$1
 PLATFORM=$2
 TARGET=$3
 cd openssl-1.1.0e
 export PLATFORM=$PLATFORM
 export CROSS_TOP="${DEVELOPER}/Platforms/${PLATFORM}.platform/Developer"
 export CROSS_SDK="${PLATFORM}${SDK_VERSION}.sdk"
 export BUILD_TOOLS="${DEVELOPER}"
 export CC="${BUILD_TOOLS}/usr/bin/gcc -arch ${ARCH}"
 sed -ie "s!static volatile sig_atomic_t intr_signal;!static volatile intr_signal;!" "crypto/ui/ui_openssl.c"
 ./Configure ${TARGET} no-asm no-unit-test
 sed -ie "s!^CFLAG=!CFLAG=-isysroot ${CROSS_TOP}/SDKs/${CROSS_SDK} -miphoneos-version-min=8.0 !" "Makefile"
 make clean
 make -j4
 mv libssl.a libssl-${ARCH}.a
   mv libcrypto.a libcrypto-${ARCH}.a
   cd ..
}

_compileIOS "arm64" "iPhoneOS" "iphoneos-cross"
_compileIOS "armv7" "iPhoneOS" "iphoneos-cross"
_compileIOS "x86_64" "iPhoneSimulator" "darwin64-x86_64-cc"
_compileIOS "i386" "iPhoneSimulator" "iphoneos-cross"

lipo -create openssl-1.1.0e/libssl-arm64.a openssl-1.1.0e/libssl-armv7.a openssl-1.1.0e/libssl-i386.a openssl-1.1.0e/libssl-x86_64.a -output out/openssl/ios/libssl.a
lipo -create openssl-1.1.0e/libcrypto-arm64.a openssl-1.1.0e/libcrypto-armv7.a openssl-1.1.0e/libcrypto-i386.a openssl-1.1.0e/libcrypto-x86_64.a -output out/openssl/ios/libcrypto.a

echo "done"

这里需要注意是,要设置最低的兼容版本为 8.0,否则在实际使用时,就蛋疼了。这里同时也编译了供 mac 使用的版本。

Linux 编译和服务器端 JNI 程序的使用

要在 Linux 上完成编译,脚本相对简单,直接使用以下代码就好了:

#!/bin/sh

if [ ! -f "openssl-1.1.0e.tar.gz" ]; then
   wget https://www.openssl.org/source/openssl-1.1.0e.tar.gz
fi

if [ ! -d "openssl-1.1.0e" ]; then
   tar zxf openssl-1.1.0e.tar.gz
fi

cd openssl-1.1.0e/
./config
make clean
make -j4
cd ..

echo "done"

编译后会得到 libssl.a 和 libcrypto.a,这两个库即可用于静态链接,同样的也可以用于 JNI。

准备都做好之后,用一个简单的算法调用来进行验证,下面就以 AES 为例吧:

int aes_encrypt(const char* in, char* key, char* out) {
   if(!in || !key || !out) return 0;
   AES_KEY aes;
   if(AES_set_encrypt_key((unsigned char*)key, 128, &aes) < 0) {
       return 0;
   }
   size_t len = strlen(in);
   int en_len = 0;
   while(en_len < len) {
       AES_encrypt((unsigned char*)in, (unsigned char*)out, &aes);
       in += AES_BLOCK_SIZE;
       out += AES_BLOCK_SIZE;
       en_len += AES_BLOCK_SIZE;
   }
   return 1;
}

int aes_decrypt(const char* in, char* key, char* out) {
   if(!in || !key || !out) return 0;
   AES_KEY aes;
   if(AES_set_decrypt_key((unsigned char*)key, 128, &aes) < 0) {
       return 0;
   }
   size_t len = strlen(in);
   int en_len = 0;
   while(en_len < len) {
       AES_decrypt((unsigned char*)in, (unsigned char*)out, &aes);
       in += AES_BLOCK_SIZE;
       out += AES_BLOCK_SIZE;
       en_len += AES_BLOCK_SIZE;
   }
   return 1;
}

char* encode(const char* key, const char* text) {
   size_t len = strlen(text) * 8;
   char sourceStringTemp[len];
   char destStringTemp[len];
   memset((char*)sourceStringTemp, 0, len);      // fillchar
   memset((char*)destStringTemp, 0, len);        // fillchar
   strcpy((char*)sourceStringTemp, text);
   char k[len];
   memset((char*)k, 0, len);
   strcpy((char*)k, key);
   char finalDest[len];
   memset((char*)finalDest, 0, len);
   char f[3];
   if (aes_encrypt(sourceStringTemp, k, destStringTemp)) {
       for(int i= 0;destStringTemp[i];i+=1){
           memset((char*)f, 0, 3);
           sprintf(f, "%0x", (unsigned char)destStringTemp[i]);
           strcat(finalDest, f);
           printf("%0x ", (unsigned char)destStringTemp[i]);
       }
       printf("\n");
   }
   char* ret = (char*)malloc(len);
   strcpy(ret, finalDest);
   return ret;
}

char* decode(const char* key, const char* text) {
   size_t len = strlen(text) * 8;
   char sourceStringTemp[len];
   char destStringTemp[len];
   memset((char*)sourceStringTemp, 0, len);      // fillchar
   memset((char*)destStringTemp, 0, len);        // fillchar
   char k[len];
   memset((char*)k, 0, len);
   strcpy((char*)k, key);
   
   // read encoded string
   int step = 0;
   int idx = 0;
   char f[5];
   int nValude = 0;
   char tmp[len];
   memset((char*)tmp, 0, len);
   strcpy((char*)tmp, text);
   while (1) {
       memset((char*)f, 0, 5);
       f[0] = '0';
       f[1] = 'x';
       f[2] = tmp[idx];
       f[3] = tmp[idx + 1];
       sscanf(f, "%x", &nValude);
       printf("%0x ", nValude);
       sourceStringTemp[step] = nValude;
       idx += 2;
       step++;
       if (!tmp[idx]) {
           break;
       }
   }
   printf("\n");
   
   aes_decrypt(sourceStringTemp, k, destStringTemp);
   char* ret = (char*)malloc(len);
   strcpy(ret, destStringTemp);
   return ret;
}

首先实现简单的函数,然后就是调用,调用的时候是相当简单的:

const char* text = "abcdefg";
const char* key = "123456";
char* enc = encode(key, text);
char* dec = decode(key, enc);
printf("origin => %s\nenc => %s\ndec => %s\n", text, enc, dec);
free(enc);
free(dec);

要实现 JNI 的话,也是非常简单,服务器端程序是 Java 的话会很容易使用,只需要导出一个头部,并做一些简单的修改即可:

#include <jni.h>

#ifndef _java_exported_
#define _java_exported_

#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jstring JNICALL Java_com_sample_AES_encode(JNIEnv*, jclass, jstring, jstring);
JNIEXPORT jstring JNICALL Java_com_sample_AES_decode(JNIEnv*, jclass, jstring, jstring);

#ifdef __cplusplus
}
#endif
#endif

再写一个 Java 的引用文件即可:

package com.sample;

public class AES {

   static {
       try {
           System.loadLibrary("sample");
       } catch (Throwable th) {
           System.out.println("load error => " + th.getMessage());
       }
   }

   public static native String encode(String key, String text);
   public static native String decode(String key, String text);

}

最后就是编译,带有 JNI 的情况下,必须手动找 jni.h 的路径,因此编译脚本是这样的:

#!/bin/sh

JNIPATH=/usr/lib/jvm/java-8-openjdk-amd64/include
g++ main.cpp \
   -Iopenssl \
   -I${JNIPATH} \
   -I${JNIPATH}/linux \
   -L. -lcrypto \
   -shared -fpic \
   -o libsample.so

再给一个 Java 端调用的脚本吧,这样就完整了:

#!/bin/sh

JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
JRE_HOME=${JAVA_HOME}/jre
CLASSPATH=.:${JAVA_HOME}/lib/dt.jar:${JAVA_HOME}/lib/tools.jar

export JAVA_HOME=${JAVA_HOME}
export JRE_HOME=${JRE_HOME}
export CLASSPATH=${CLASSPATH}
export LD_LIBRARY_PATH=.:${LD_LIBRARY_PATH}

cd com/sample
rm *.class
javac *.java
cd ../../

java com/sample/Main

End

推荐阅读

教你使用 WKWebView 的正确姿势

翻译 | 老司机带你秒懂内存管理!

翻译干货!开始使用 TypeScript 和 React

沪江开发的未解之谜