一開始聽到這個需求挺懵的,作為一個聊天軟件,代碼里并沒有所謂核心算法和商業(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ù)方案討論由來已久。
總結(jié)下來,官方并沒有打算提供解決方案。作者們認(rèn)為,無論用什么形式去加密打包文件,密鑰總歸是需要放置在包里的。。
繼續(xù)翻,國內(nèi)論壇上一些大佬有嘗試解決過這個問題,是從asar打包這塊切入,然而,并沒有看懂。。
簡單理解下大佬的思路:對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ù)上如何取舍吧。