前言
众说周知,Electron
本身没有提供任何源码保护方案,只有一个ASAR
的打包方式。
以下简单罗列下几个探索的方向
Asar
ASAR 表示 Atom Shell Archive Format。 一个 asar 档案就是一个简单的
tar
文件 - 比如将那些有关联的文件放至一个单独的文件格式中。 Electron 能够任意读取其中的文件并且不需要解压整个文件。ASAR格式是为了在Windows系统读取大量的小文件时 (比如像从
node_modules
加载应用的JavaScript依赖关系树) 提高性能。
ASAR
本意不是用来做源码加密的,只是一个副作用, 而且只能防小白,不能防了解Electron
的开发者。
破解之道
npm i asar -g
asar e app.asar /some/path
代码混淆
- 经典前端使用套路,只是增加阅读成本,不能保护源码
V8-Code-Cache
code cache 是 V8 中的一个特性,简单说就是 JavaScript 代码在执行前,取消进行解析和编译,才能正确执行,解析编译过程是耗时的,V8 暴露了一个方法,可以将编译产物序列化存储下来,下次再执行相同一段代码时,就可以用之前缓存的内容,节省了解析编译的时间
根据描述,code cache是源码编译生成的字节码,无法还原为源码,我们是否可以使用这个制定我们的加密方案呢? “理论可以”
步骤如下:
- 打包时候(客户端构建):生成codecahe
- 打包时候(客户端构建):修改原始js
- 运行时候加载
打包时生成CodeCache
// 加密函数
function createCachedData (filename, content) {
var wrapper = Module.wrap(content)
var script = new vm.Script(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true,
produceCachedData: true,
});
return script.createCachedData()
}
//...
// 对所有.js遍历执行
let cachedData = createCachedData(filename, filedata)
// 保存下来
fs.writeFileSync(savefilename + '.cache', filedata)
loader.js(加载器)
- 通过扩展原始
require
方法,支持加载.cache
文件
const path = require('path');
const vm = require('vm');
const fs = require('fs');
const Module = require('module');
const crypto = require('crypto');
Module.prototype._compile = function(content, filename) {
const mod = this;
function require(id) {
return mod.require(id);
}
// // https://github.com/nodejs/node/blob/v10.15.3/lib/internal/modules/cjs/helpers.js#L28
function resolve(request, options) {
return Module._resolveFilename(request, mod, false, options);
}
require.resolve = resolve;
require.main = process.mainModule;
// // Enable support to add extra extension types
require.extensions = Module._extensions;
require.cache = Module._cache;
var cachedData
var invalidationKey
const dirname = path.dirname(filename);
const hash = /\.cache$/.test(filename) && path.basename(filename, '.source')
console.log(filename)
if (hash) {
invalidationKey = hash
} else {
invalidationKey = crypto
.createHash('sha1')
.update(filename, 'utf8')
.digest('hex');
}
var p = path.join(process.resourcesPath, 'xx', invalidationKey) // xx替换为正确的路径
if (fs.existsSync(p)) {
cachedData = fs.readFileSync(p)
}
var wrapper = Module.wrap(content)
var script = new vm.Script(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true,
cachedData: cachedData,
});
if (!cachedData || script.cachedDataRejected) {
fs.writeFileSync(p, script.createCachedData())
}
var compiledWrapper = script.runInThisContext({
filename: filename,
lineOffset: 0,
columnOffset: 0,
displayErrors: true,
});
// console.log(dirname)
const args = [mod.exports, require, mod, filename, dirname, process, global, Buffer];
return compiledWrapper.apply(mod.exports, args)
}
覆盖原始的js
require(resourcesDirPath + '/loader.js')
require(savefilename + '.cache' )
小结
- 以上代码是我亲自写,但是有点历史,大家只能借鉴不能完全拷贝使用。(当时发现,生成的codecache不能在其他机器使用,没有往下分析)
扩展阅读
魔改Asar
- 该方法,就是通过修改
Electron
原始代码,改变asar
的的逻辑,无法通过npm的asar解密。(飞书之前Electron版本就是通过这方法)。但是这个方法要求有较强的C语言开发能力,修改和编译Electron,小团队无法复制。
大器Native Addon
通过原生组件加密,原生组件解密并在底层v8引擎执行达到目的。(文件都是加密否的二进制文件)
- 通过C语言,开发原生组件Addon
- 源码编译(客户端打包):加密源码并生产密文文件,同时替换原始文件(该步骤类似CodeCache生成,只不过加密是自定义的)
- 运行时:通过原生组件Addon,加载密文文件,并在底层v8引擎执行
生成的.js内容如下
var loadjs = require(process.resourcesPath + '/app.asar.unpacked/node_modules/@xxx_encrypt');
loadjs.execute(filename , require, __filename, __dirname, process);