Electron-如何保護原始碼?

Charles0427發表於2019-02-18

一開始聽到這個需求挺懵的,作為一個聊天軟體,程式碼裡並沒有所謂核心演算法和商業機密,為什麼需要保護原始碼。況且Electron本身在打包時提供了asar這種archive檔案格式,會將所有原始碼和依賴封裝。

需求

一陣分析後,Electron專案原始碼保護還是有必要的。

  • asar只是對原始碼的合併歸檔,並不提供加密之類的操作。 通過asar e的命令,可以很簡單地進行解壓和得到原始碼。
  • 業務上,即時通訊應用的聊天資料均儲存在本地,雖然使用了加密版的sqlite3。但拿到原始碼,也就意味著知道了金鑰,資料庫加密也就形同虛設。

尋找解決方案

asar加密

翻github和Stack Overflow,發現對Electron原始碼保護方案討論由來已久。

Source Code Protection #3041

Add Encryption Feature #46

總結下來,官方並沒有打算提供解決方案。作者們認為,無論用什麼形式去加密打包檔案,金鑰總歸是需要放置在包裡的。。

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

electron 加密打包的正確方法

簡單理解下大佬的思路:對asar原始碼進行分析,在 asar 打包時寫入檔案之前, 通過加密演算法把寫入的檔案進行加密;在asar.js讀取檔案處新增對應檔案解密演算法;同時對asar檔案頭部 json 進行加密,使得官方的 asar 就沒法解包了。

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

addons封裝核心程式碼

electron issue裡有人提出可以利用nodejs的addons來封裝核心程式碼。addons是nodejs實現跨平臺呼叫原生程式碼的外掛,因為保護原始碼的主要目的是為了提高安全性,將資料庫金鑰等關鍵欄位儲存在原生程式碼中,提高破解門檻。

實現

C++語法基本忘光了,先實現一個簡單業務練練手:JS傳入使用者資訊物件,C++讀取物件,處理後,返回資料庫對應的金鑰。

Nodejs與C++之間的型別轉換由V8 API提供,具體可參考Node.js 和 C++ 之間的型別轉換

// 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傳遞的引數是否為物件
  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的物件和獲取單獨使用者的key,當然,這裡只是簡單的業務邏輯展示。在Nodejs Addons中,介面是通過這種模式的初始化函式:

void Initialize(Local<Object> exports);
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
複製程式碼

NODE_GYP_MODULE_NAME,是在binding.gyp中設定的模組名稱。Nodejs不能直接呼叫C++檔案,需要先通過node-gyp將其編譯為二進位制檔案,binding.gyp則是類似JSON格式的構建配置檔案。在根目錄下新建該檔案:

{
  "targets": [
    {
      "target_name": "dbkey",
      "sources": [ "key.cc" ]
    }
  ]
}
複製程式碼

安裝好node-gyp和相關依賴。先後輸入命令 node-gyp configurenode-gyp build

成功後,生成build目錄,得到二進位制檔案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(),我們就可以呼叫C++模組。

但此時的dbkey.node並不能直接扔進electron中使用,我們需要用electron相關標頭檔案對該外掛進行重編譯。

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

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

ps. 因為Nodejs版本很多,其V8 API也不完全一致,C++邏輯建議使用NAN,NAN對V8 API做了封裝,使我們不用關心版本問題。我們專案中使用的Native模組,如canvas,sqlite等,其原始碼也都是使用NAN。

總結

利用C++ Addons封裝核心業務程式碼,能一定程度提升原始碼的安全性。但需要修改之前的打包流程,開發除錯上也會帶來一些不便。還是看業務上如何取捨吧。

相關文章