Electron如何保護(hù)源碼

一開始聽到這個需求挺懵的,作為一個聊天軟件,代碼里并沒有所謂核心算法和商業(yè)機(jī)密,為什么需要保護(hù)源碼。況且Electron本身在打包時提供了asar這種archive文件格式,會將所有源碼和依賴封裝。

需求

一陣分析后,Electron項(xiàng)目源碼保護(hù)還是有必要的。

  • asar只是對源碼的合并歸檔,并不提供加密之類的操作。
    通過asar e的命令,可以很簡單地進(jìn)行解壓和得到源碼。
  • 業(yè)務(wù)上,即時通訊應(yīng)用的聊天數(shù)據(jù)均存儲在本地,雖然使用了加密版的sqlite3。但拿到源碼,也就意味著知道了密鑰,數(shù)據(jù)庫加密也就形同虛設(shè)。

尋找解決方案

asar加密

翻github和Stack Overflow,發(fā)現(xiàn)對Electron源碼保護(hù)方案討論由來已久。

Source Code Protection #3041

Add Encryption Feature #46

總結(jié)下來,官方并沒有打算提供解決方案。作者們認(rèn)為,無論用什么形式去加密打包文件,密鑰總歸是需要放置在包里的。。

繼續(xù)翻,國內(nèi)論壇上一些大佬有嘗試解決過這個問題,是從asar打包這塊切入,然而,并沒有看懂。。

electron 加密打包的正確方法

簡單理解下大佬的思路:對asar源碼進(jìn)行分析,在 asar 打包時寫入文件之前, 通過加密算法把寫入的文件進(jìn)行加密;在asar.js讀取文件處添加對應(yīng)文件解密算法;同時對asar文件頭部 json 進(jìn)行加密,使得官方的 asar 就沒法解包了。

思路我是看懂了,怎么下手完全不知。有興趣的童鞋可以主動去留言詢問。。

addons封裝核心代碼

electron issue里有人提出可以利用nodejs的addons來封裝核心代碼。addons是nodejs實(shí)現(xiàn)跨平臺調(diào)用原生代碼的插件,因?yàn)楸Wo(hù)源碼的主要目的是為了提高安全性,將數(shù)據(jù)庫密鑰等關(guān)鍵字段存儲在原生代碼中,提高破解門檻。

實(shí)現(xiàn)

C++語法基本忘光了,先實(shí)現(xiàn)一個簡單業(yè)務(wù)練練手:JS傳入用戶信息對象,C++讀取對象,處理后,返回數(shù)據(jù)庫對應(yīng)的密鑰。

Nodejs與C++之間的類型轉(zhuǎn)換由V8 API提供,具體可參考Node.js 和 C++ 之間的類型轉(zhuǎn)換。

// key.cc
#include <node.h>

namespace key {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Object;
using v8::String;
using v8::Value;

void GetKeys(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // 判斷js傳遞的參數(shù)是否為對象
  if (!args[0]->IsObject()) {
    printf("not Object\n");
  }

  // 新建對象,將cfg和id綁定到對象
  Local<String> cfgKey = v8::String::NewFromUtf8(isolate, "testxxx");
  Local<Object> keyObj = v8::Object::New(isolate);
  keyObj->Set(v8::String::NewFromUtf8(isolate, "cfgKey"), cfgKey);

  // 讀取js傳遞的對象
  Local<Object> userObj = Local<Object>::Cast(args[0]);
  Local<Value> id = userObj->Get(String::NewFromUtf8(isolate, "id"));
  keyObj->Set(v8::String::NewFromUtf8(isolate, "id"), id);

  args.GetReturnValue().Set(keyObj);
}

void GetUidByUserInfo(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  Local<Object> userObj = Local<Object>::Cast(args[0]);
  Local<Value> id = userObj->Get(String::NewFromUtf8(isolate, "id"));

  args.GetReturnValue().Set(id);
}

void Initialize(Local<Object> exports) {
  NODE_SET_METHOD(exports, "getKey", GetKeys);
  NODE_SET_METHOD(exports, "getUserKey", GetUidByUserInfo);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

} 

key.cc中暴露了兩個簡單的方法,分別是獲取所有key的對象和獲取單獨(dú)用戶的key,當(dāng)然,這里只是簡單的業(yè)務(wù)邏輯展示。在Nodejs Addons中,接口是通過這種模式的初始化函數(shù):

void Initialize(Local<Object> exports);
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

NODE_GYP_MODULE_NAME,是在binding.gyp中設(shè)定的模塊名稱。Nodejs不能直接調(diào)用C++文件,需要先通過node-gyp將其編譯為二進(jìn)制文件,binding.gyp則是類似JSON格式的構(gòu)建配置文件。在根目錄下新建該文件:

{
  "targets": [
    {
      "target_name": "dbkey",
      "sources": [ "key.cc" ]
    }
  ]
}

安裝好node-gyp和相關(guān)依賴。先后輸入命令
node-gyp configure
node-gyp build

成功后,生成build目錄,得到二進(jìn)制文件dbkey.node。

然后,我們寫個js測試下。

const dbKey = require('./build/Release/dbkey');

const userInfo = {
  id: '123456',
};

console.log(dbKey.getKey()); // { cfgKey: 'testxxx', id: '123456' }
console.log(dbKey.getUserKey(userInfo)); // 123456

通過require(),我們就可以調(diào)用C++模塊。

但此時的dbkey.node并不能直接扔進(jìn)electron中使用,我們需要用electron相關(guān)頭文件對該插件進(jìn)行重編譯。

node-gyp rebuild --target=1.7.11 --arch=x64 --target_platform=darwin --dist-url=https://atom.io/download/atom-shell

根據(jù)你的electron版本號(target)和平臺(target_platform)分別重編譯。

ps. 因?yàn)镹odejs版本很多,其V8 API也不完全一致,C++邏輯建議使用NAN,NAN對V8 API做了封裝,使我們不用關(guān)心版本問題。我們項(xiàng)目中使用的Native模塊,如canvas,sqlite等,其源碼也都是使用NAN。

總結(jié)

利用C++ Addons封裝核心業(yè)務(wù)代碼,能一定程度提升源碼的安全性。但需要修改之前的打包流程,開發(fā)調(diào)試上也會帶來一些不便。還是看業(yè)務(wù)上如何取舍吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容