基于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),致力于提供基于去中心化網絡技術實現的分布式存儲和分布式計算平臺,具有高可用、低功耗和低網絡的技術特點,適用于物聯網、工業互聯網等場景。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容