基于wasm的openssl实践

上一篇文章分享了WebAssembly概念和基本使用,通过两个代码示例的分析对WebAssembly有了大致的了解。这一篇文章分享的是基于WebAssembly的加密工具实践,我们就以openssl的摘要算法md5和sha1为例,在Mac上编译openSSL到WebAssembly。

环境

  • Emscripten 版本 2.0.3
  • Openssl 版本1.1.1d
  • 浏览器 版本 85.0.4183.121(正式版本) (64 位)

概述

  • 在Mac上编译openSSL到WebAssembly
  • 遇到的问题
  • 总结

一、在Mac上编译openSSL到WebAssembly

将Openssl编译到WebAssembly整个流程是这样的,md5.c文件–>emscripten编译–>.wasm文件–>结合WebAssembly JS API–>浏览器中运行。

1. md5.c文件
//md5.c
#include <emscripten.h>
#include <openssl/md5.h>
#include <openssl/sha.h>
#include <string.h>
#include <stdio.h>

EMSCRIPTEN_KEEPALIVE
void md5(char *str, char *result,int strlen) {
    MD5_CTX md5_ctx;
    int MD5_BYTES = 16;
    unsigned char md5sum[MD5_BYTES];
    MD5_Init(&md5_ctx);  
    MD5_Update(&md5_ctx, str,strlen);
    MD5_Final(md5sum, &md5_ctx);
    char temp[3] = {0};
    memset(result,0, sizeof(char) * 32);
    for (int i = 0; i < MD5_BYTES; i++) {
        sprintf(temp, "%02x", md5sum[i]);
        strcat(result, temp);
    }
    result[32] = '\0';
}

EMSCRIPTEN_KEEPALIVE
void sha1(char *str, char result[],int strlen) {
    unsigned char digest[SHA_DIGEST_LENGTH];
    SHA_CTX ctx;
    SHA1_Init(&ctx);
    SHA1_Update(&ctx, str, strlen);
    SHA1_Final(digest, &ctx);
    for (int i = 0; i < SHA_DIGEST_LENGTH; i++){
        sprintf(&result[i*2], "%02x", (unsigned int)digest[i]);
    }
}

md5.c文件中包含了md5和sha1两个函数,后面会用来编译到wasm。

Tips: 
1. 默认情况下,Emscripten 生成的代码只会调用 main() 函数,其它的函数将被视为无用代码。在一个函数名之前添加 EMSCRIPTEN_KEEPALIVE 能够防止这样的事情发生。你需要导入 emscripten.h 库来使用 EMSCRIPTEN_KEEPALIVE。
2. 内部实现调用的是openssl提供的函数,简单封装下直接调用即可。
2. Emscripten编译
下载openssl,生成Makefile

我用的openssl版本是1.1.1d,地址: https://github.com/openssl/openssl/releases/tag/OpenSSL_1_1_1d
解压后,进入openssl-OpenSSL_1_1_1d文件夹。编译生成Makefile文件。

emcmake ./Configure  darwin64-x86_64-cc -no-asm --api=1.1.0

修改生成的Makefile文件,如果不修改,容易出现编译错误。

  • 将CROSS_COMPILE=/usr/local/Cellar/emscripten/1.38.44/libexec/em 改为 CROSS_COMPILE=
  • 将 CNF_CFLAGS=-arch x86_64 改为 CNF_CFLAGS=
编译openssl
emmake make -j 12 build_generated libssl.a libcrypto.a
mkdir -p ~/resource/openssl/libs
cp -R include ~/resource/openssl/include
cp libcrypto.a libssl.a ~/Downloads/openssl/libs

创建了一个openssl目录,其实是为了在md5.c中引用静态库的位置。编译成功后,文件夹下会出现libssl.a和libcrypto.a两个文件,

编译wasm
emcc md5.c -I ~/resource/openssl/include -L ~/resource/openssl/libs -lcrypto -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap", "ccall"]' -o md5.js

编译成功后,会生成md5.js和md5.wasm两个文件。

Tips: 
Emscripten从v1.38开始,ccall/cwrap辅助函数默认没有导出,在编译时需要通过-s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall', 'cwrap']"选项显式导出。
3. 调用wasm文件

使用WebAssembly JS API调用wasm。md5和sha1的代码都放在了md5.html中了,两者使用方式一样,文中只贴md5相关代码。代码地址: https://github.com/likai1130/study/blob/master/wasm/openssl/demo/md5.html

<div>
    <div>
        <input type="file" id="md5files" style="display: none" onchange="md5fileImport();">计算md5
        <input type="button" id="md5fileImport" value="导入">
    </div>
</div>

<script src="jquery-3.5.1.min.js"></script>
<script src="md5.js"></script>
<script type='text/javascript'>
    Module = {};
    const mallocByteBuffer = len => {
        const ptr = Module._malloc(len)
        const heapBytes = new Uint8Array(Module.HEAPU8.buffer, ptr, len)
        return heapBytes
    }
    //点击导入按钮,使files触发点击事件,然后完成读取文件的操作
    $("#md5fileImport").click(function() {
        $("#md5files").click();
    })
    function md5fileImport() {
        //获取读取我文件的File对象
        var selectedFile = document.getElementById('md5files').files[0];
        var name = selectedFile.name; //读取选中文件的文件名
        var size = selectedFile.size; //读取选中文件的大小
        console.log("文件名:" + name + "大小:" + size);
        var reader = new FileReader(); //读取操作就是由它完成.
        reader.readAsArrayBuffer(selectedFile)
        reader.onload = function() {
            //当读取完成后回调这个函数,然后此时文件的内容存储到了result中,直接操作即可
            console.log(reader.result);
            const md5 = Module.cwrap('md5', null, ['number', 'number'])                 const inBuffer = mallocByteBuffer(reader.result.byteLength)
            var ctx = new Uint8Array(reader.result)                 inBuffer.set(ctx)
            const outBuffer = mallocByteBuffer(32)
            md5(inBuffer.byteOffset,outBuffer.byteOffset,inBuffer.byteLength)
            console.log("md5值= ",Array.from(outBuffer).map(v => String.fromCharCode(v)).join(''))
            Module._free(inBuffer);
            Module._free(outBuffer);
        }
    }
</script>
4. 浏览器中运行

文件a.out,是个二进制数据
md5: 0d3c57ec65e81c7ff6da72472c68d95b
sha1: 9ef00799a4472c71f2177fd7254faaaadedb0807

在这里插入图片描述

[图片上传失败...(image-94dcd6-1607485189727)]
一个是程序计算的md5和sha1,一个是系统上openssl计算的md5和sha1,说明本次Webassembly编译openssl的实践是成功的。

二、遇到的问题

调用链如下:

md5.js (胶水代码)<-----> md5.c <-----> openssl API
数据通信问题

在整个实践的过程中,最令人头疼的问题是数据通信问题。在 C/C++ 和 JS 之间传递复杂数据结构很麻烦,需要操作内存来实现。

  • Javascript与C/C++交换数据

    typescript
    #md5.wasm解析后的md5函数在wasm文件中的代码
    func $md5 (;3;) (export "md5") (param $var0 i32) (param $var1 i32) (param $var2 i32)
    

因为wasm 目前只可以 import 和 export C 语言函数风格的 API,而且参数只有四种数据类型(i32, i64, f32, f64),都是数字,可以理解为赤裸裸的二进制编码,没法直接传递复杂的类型和数据结构。所以在浏览器中这些高级类型的 API 必须靠 JS 来封装,中间还需要一个机制实现跨语言转换复杂的数据结构。

  • Module.buffer

    无论编译目标是asm.js还是wasm,C/C++代码眼中的内存空间实际上对应的都是Emscripten提供的ArrayBuffer对象:Module.buffer,C/C内存地址与Module.buffer数组下标一一对应。

function md5fileImport() {
   var selectedFile =   document.getElementById('md5files').files[0];
   var name = selectedFile.name; //读取选中文件的文件名
   var size = selectedFile.size; //读取选中文件的大小
   console.log("文件名:" + name + "大小:" + size);
   var reader = new FileReader(); //这是核心,读取操作就是由它完成.
  
   reader.readAsArrayBuffer(selectedFile)
   .....
}

在代码中我们使用reader.readAsArrayBuffer()来读取文件,返回的是ArrayBuffer数组。但还是不能调用C函数,需要创建一个 typed array,如 Int8Array, UInt32Array,用其特定的格式作为这段二进制数据的 view,从而进行读写操作。

Tips:
C/C++代码能直接通过地址访问的数据全部在内存中(包括运行时堆、运行时栈),而内存对应Module.buffer对象,C/C代码能直接访问的数据事实上被限制在Module.buffer内部。

WebAssembly 的内存也是一个 ArrayBuffer,Emscripten 封装的 Module 提供了 Module.HEAP8、Module.HEAPU8 等各种 view。附图:


在这里插入图片描述
  • 在JavaScript中访问C/C++内存

计算md5/sha1需要javascript将大量数据输入到C/C++环境,而C/C++无法预知数据块的大小,此时可以在JavaScript中分配内存并装入数据,然后将数据指针传入,调用C函数进行处理。

Tips:
这种用法之所以可行,核心原因在于:Emscripten导出了C的malloc()/free()

我将分配内存空间的方法声明成了公共方法。

Module = {};
const mallocByteBuffer = len => {
    const ptr = Module._malloc(len)
    const heapBytes = new Uint8Array(Module.HEAPU8.buffer, ptr, len)
    return heapBytes
}

function md5fileImport() {
    //获取读取我文件的File对象
    var selectedFile = document.getElementById('md5files').files[0];
    ......
    var reader = new FileReader(); //这是核心,读取操作就是由它完成.
    reader.readAsArrayBuffer(selectedFile)
    reader.onload = function() {
        //当读取完成后回调这个函数,然后此时文件的内容存储到了result中,直接操作即可
        const md5 = Module.cwrap('md5', null, ['number', 'number'])
        const inBuffer = mallocByteBuffer(reader.result.byteLength)
        var ctx = new Uint8Array(reader.result)
        inBuffer.set(ctx)
        const outBuffer = mallocByteBuffer(32)
        md5(inBuffer.byteOffset,outBuffer.byteOffset,inBuffer.byteLength)

        console.log("md5值= ",Array.from(outBuffer).map(v => String.fromCharCode(v)).join(''))
        Module._free(inBuffer);
        Module._free(outBuffer);
    }
}
Tips: 
C/C++的内存没有gc机制,在JavaScript中使用malloc()函数分配的内存使用结束后,需要使用free()将其释放。

此外,Emscripten还提供了AsciiToString()/stringToAscii()/UTF8ArrayToString()/stringToUTF8Array()等一系列辅助函数用于处理各种格式的字符串在各种存储对象中的转换,欲知详情请自行参考胶水代码。

三、总结

基于wasm的openssl完整调用关系:

在这里插入图片描述

本次实践过程中遇到的技术问题就是数据通信的问题,还有一个是思路上的问题,一直以为把openssl整体编译成.wasm文件,就可以用了,事实证明还需要使用胶水代码,才能在web中使用。那么有个疑问.wasm文件本质上是个二进制文件,是否有工具可以直接运行呢.wasm文件,WAPM(WebAssembly Package Manager) 这是WebAssembly的包管理工具,下一篇文章一起来认识下WebAssembly包管理工具。

参考资料


Netwarps 由国内资深的云计算和分布式技术开发团队组成,该团队在金融、电力、通信及互联网行业有非常丰富的落地经验。Netwarps 目前在深圳、北京均设立了研发中心,团队规模30+,其中大部分为具备十年以上开发经验的技术人员,分别来自互联网、金融、云计算、区块链以及科研机构等专业领域。
Netwarps 专注于安全存储技术产品的研发与应用,主要产品有去中心化文件系统(DFS)、去中心化计算平台(DCP),致力于提供基于去中心化网络技术实现的分布式存储和分布式计算平台,具有高可用、低功耗和低网络的技术特点,适用于物联网、工业互联网等场景。

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容