查看原文
Software Development

Electron 加密:让你的源代码更安全

lencx 浮之静 2023-08-23

如果你正在使用 Electron 构建跨平台桌面应用,担心源码泄露,那么这个系列或许就是你需要寻找的东西。

Electron 是一个使用 JavaScript, HTML 和 CSS 构建跨平台桌面应用的框架。在 Electron 中,会包含 Main、Preload 和 Renderer 三层:

  • Main 进程:

    • Main 进程是 Electron 应用程序的入口点,它负责控制应用的整个生命周期,管理所有的渲染进程,并且与原生系统进行交互。这个进程运行在 Node.js 环境下,所以可以访问所有的 Node.js API。

    • Main 进程可以创建和控制 BrowserWindow 实例,每个 BrowserWindow 实例都运行着自己的渲染进程。

  • Renderer 进程:

    • Renderer 进程是在 BrowserWindow 中运行的,负责渲染应用的界面。

    • 渲染进程中运行的代码与浏览器环境相似,但通过在渲染进程中禁用或启用 Node.js 集成,可以控制其对 Node.js API 的访问权限。

    • 由于渲染进程可以处理和操作 DOM,所以通常在此进程中加载和运行与用户界面相关的代码。

  • Preload 脚本:

    • Preload 脚本是一种在渲染进程之间建立桥梁的机制,允许你在渲染进程中安全地使用某些 Node.js 特性。

    • 它允许你在加载页面前运行一些预定义的脚本,这样你可以为渲染进程中的页面注入某些特定功能或者限制某些功能的访问。

    • Preload 脚本常用于在不直接暴露全部 Node.js API 的同时,向渲染进程中安全地提供某些特定的功能或方法。

总的来说,Main 进程负责管理应用的全局逻辑,Renderer 进程负责界面渲染,而 Preload 脚本允许你在这两者之间安全地传递和控制功能。而常说的代码泄漏往往是指这三层,那有没有一种方案,可以做到三层完全保护呢?我的回答是有的,这个系列教程将带你开启源码保护之旅。文末获取进交流群方式,付费后即可进入(在此系列中源码保护只是一个起点,未来不排除分享其他小技巧,总之干货满满)。

背景

熟悉我的朋友应该都清楚,我之前是 Tauri 的狂热信徒,在教程资源(尤其是中文)还不是那么丰富的时候,我就写过一个 Tauri 系列比较系统的介绍了它方方面面。

我在 2022.12.07 基于 Tauri 构建的 ChatGPT[1] 开源桌面应用,更是多次霸榜 GitHub Trending。目前已经收获了 40.4k+ stars,340+ 万次累计下载量。

随着项目用户的增多,我发现了许多问题(兼容性 BUG)。众所周知,Tauri 是借助系统 WebView 来渲染的,所以安装包不需要捆绑 WebView 容器,可以做到超小的打包体积(一般 Hello World 应用在 2-3M)。但是不捆绑 WebView 的方案即是天使也是魔鬼,在享受小体积的同时需要忍受不同系统 WebView 的大坑(用户量越大,越容易出问题)。

Tauri 之大坑

在聊 Tauri WebView 会遇到哪些问题时,我们先要对它的架构有一个简单理解。Tauri 其实是一个胶水层,提供跨平台打包方案,主要是由它上游维护的 WRY 和 TAO 两个依赖库实现。

  • WRY[2]:Rust 中的跨平台 WebView 渲染库,支持所有主要桌面平台(Windows、macOS 和 Linux)。

  • TAO[3]:Rust 中的跨平台应用程序窗口创建库,支持所有主要平台(Windows、macOS、Linux、iOS 和 Android)。

问题就出在 WRY 上,因为它直接绑定操作系统的 WebView,有些问题它处理起来是真的无能为力。例如在 macOS 上是 WebKit[4],在 Windows 上是 WebView2[5],而在 Linux 上则是 WebKitGTK[6]。除了不同操作系统的 WebView 实现不一致外,WebView 的版本也会导致一些奇怪的 BUG(系统的 WebView 版本一般会根据操作系统版本进行升级)。

可以设想一下,如果你正在制作一个服务于上百万用户的应用,用户的系统及其系统版本必然是五花八门,这一切的兼容性都会让你陷入兼容性黑洞。有些 BUG 是系统或者其版本所固有的,你难道要让用户换系统或者升级系统吗?这是不现实的,也就导致了一些奇怪的 BUG 往往是无解的。

Electron 会是灵药吗?

之前学习 Tauri 时更多是为了好玩(也是为了学习 Rust,一颗折腾的心),所以只是在自己的电脑上跑起一个应用或工具,这些都是轻而易举的。但当面对用户大量的兼容性 BUG (ChatGPT issues[7])反馈时,我开始怀疑自己的技术方案选择了,很多问题都是莫名其妙,甚至是无解(同样的操作系统,同样的系统版本,就是有的人显示正常,有的人则显示不正常,有点人格分裂...)。

就在这时我开始尝试重新去了解 Electron(以前玩过 Demo,但没有过多深入)。虽然我很喜欢 Tauri + Rust。但是想做更多的事情,稳定可靠是前提。或许可以考虑 Electron + WebAssembly,这样还能写一点 Rust(Rust 编译为 WebAssembly)。

一个不切实际的想法

Tauri 上游依赖 WRY 目前在疯狂适配各种系统 WebView,所以 WebView 自身存在的问题它并不能改善或解决。要是 Tauri 支持切换 WebView 到 Chromium(或者某个稳定的跨平台渲染层)就有趣了。因为相比于安装包体积的减小,稳定性和后端使用 Rust 才是 Tauri 最大的吸引力(相比于 Electron 的 Node.js)。

没有银弹

所以现在,我对平台和工具的理解:

  • 🚀 平台:模块多,生态,Electron 适合

  • 🛠️ 工具:模块少,专一,Tauri 适合(对 WebView 依赖较小)

举个例子:VS Code 就是平台,因为它有完善的插件体系来拓展平台功能。要支撑起生态,平台基座的稳定性就显得很重要了,应用安装包体积在生态面前是可以被忽略的,随着模块增多,这个缺点会不断缩小。

初识 Electron

Electron 我最近也折腾了不少,解决了很多核心技术问题。比如 Electron 源码安全问题,默认不会对源码进行任何额外操作(最多就是 asar 一下,或者使用前端打包工具 webapck,vite 之类的对代码进行简单的混淆压缩)。而在这一方面,Tauri 的打包就要安全许多,它会将整个源代码打包为二进制文件。

源码安全

使用 Electron 打包桌面应用时,安装包中打包一份 asar 文件,对 asar 文件进行解压,可以看到应用源代码。这是所有常规 Electron 应用都会遇到的问题,据我所知,目前社区有两种方案,一种是直接将 js 文件编译成 v8 字节码(如 Bytenode[8] 则将 js 源码编译为 v8 字节码,因为 js 是动态语言,所以一般认为此过程不可逆),另一种是对 js 文件进行压缩混淆加密(如 JavaScript obfuscator[9] 它会将变量方法名替换为随机字符串,增加阅读难度。此过程可逆,并不安全)。

📌 Electron ASAR

Electron ASAR[10] 是 Electron 使用的一种简单的归档格式,类似于 .tar。ASAR,即 Atom Shell Archive Format,是一种将多个文件合并到一个文件中的格式,而不会影响在 Electron 应用程序中的执行。

Electron 通过 ASAR 实现了以下特性:

  • 更快的应用程序启动时间,因为只需要读取一个文件

  • 更容易的应用程序分发,因为只需要分发一个文件

  • 可以隔离应用程序的源代码,使其不易被用户直接访问或修改

然而,ASAR 也有其限制。例如,一些 Node.js 的 API,如 fs.readFile,无法直接在 ASAR 归档文件中读取单个文件。为了解决这个问题,Electron 提供了一种方式来从 ASAR 归档中读取文件,通过将文件路径中的 .asar 替换为 .asar.unpacked

你可以使用 Electron 自带的 asar 命令行工具创建和解压 ASAR 归档文件,或者在 Node.js 中直接使用 asar 包操作 ASAR 归档。

📌 Google V8

V8[11] 是 Google 开发的开源 JavaScript 引擎,用于 Google Chrome 和 Chromium 网络浏览器。V8 引擎将 JavaScript 代码编译成更底层的机器代码,然后在计算机上执行。这样做的主要目的是为了提高代码的执行效率。

V8 字节码(V8 Bytecode)是 V8 引擎中的一个概念。在最初的实现中,V8 使用即时(JIT)编译将 JavaScript 代码直接转换为机器代码。然而,这种方法在处理复杂的 JavaScript 代码或在内存受限的设备上可能会出现效率问题。

为了解决这个问题,V8 引擎的新版本引入了字节码解释器 Ignition[12]。在这个模型中,JavaScript 代码首先被编译成一种中间的字节码,然后由 Ignition 解释器执行这些字节码。这种方式可以更有效地利用内存,并允许更高级的代码优化。

字节码通常不会被直接用于执行,但在某些情况下,例如当代码只执行一次或者为了提高启动速度时,它可以被直接执行。字节码还可以被进一步编译为机器代码,以提高频繁执行的代码的性能,这是由 V8 的优化编译器 TurboFan[13] 完成的。

所以说,V8 字节码是 V8 JavaScript 引擎中用于提高性能和内存效率的一个重要组成部分。


V8 字节码主要是为了提高 JavaScript 的执行效率,而不是为了保护源代码。事实上,一旦 JavaScript 代码被浏览器加载,它就可以通过各种开发者工具进行查看和调试,无论它是否被编译成了 V8 字节码。

尽管如此,你可以通过一些方式来增加对源代码的保护,例如:

  • 混淆:这是一个过程,它会修改你的源代码以使其更难阅读和理解。混淆可以改变变量名、函数名、和/或其他代码元素,以使它们失去原本的意义。

  • 压缩:这个过程移除源代码中的所有不必要的字符(如空格、换行符和注释),从而减小文件的大小。这不仅可以提高代码的载入速度,还可以使代码更难阅读。

  • 代码加密:有些工具可以将 JavaScript 代码转换为一种加密形式,该形式需要一个特殊的解密密钥才能执行。这可以在一定程度上阻止未经授权的人员阅读源代码,但也会增加代码执行的复杂性。

但是需要注意的是,以上提到的所有方法都无法提供完全的源代码保护。JavaScript 是一种客户端脚本语言,意味着它必须在用户的浏览器上执行。无论代码被混淆、压缩还是加密,最终都需要转换为可执行的 JavaScript 代码。因此,有足够技术水平的人仍然可以通过一定的工具和技术来“反混淆”、“反压缩”或者“反加密”代码。


V8 字节码在理论上是可以被逆向的,这意味着可以将其转换回源代码形式。然而,逆向工程通常是一个复杂的过程,需要深入的专业知识,并且即使是在理想的情况下,也很难(如果不是不可能的话)完全复原源代码。这是因为编译过程通常会丢失某些源代码的信息,比如注释和某些编程构造可能会被优化掉。

对于 JavaScript 和 V8 引擎来说,有几个因素可以使得逆向工程更加困难:

  • JavaScript 是一种动态类型语言,这意味着在编译时并不知道变量的确切类型。这在逆向工程过程中可能会导致问题,因为字节码不包含这种类型信息。

  • V8 引擎使用了许多优化策略,比如内联函数和消除死代码。这些优化可能会改变字节码的结构,使得它不再直接对应源代码。

  • V8 的字节码设计为解释执行,而不是为了容易地反编译。这意味着字节码的结构可能不直观,且难以手动解析。

所以,虽然理论上可能,但在实践中使用 V8 字节码进行逆向工程是一个极其复杂的任务,往往需要深入的专业知识和大量的时间。

社区方案

注意:这里所说的源码加密并非真正的加密,而是使逆向难度增高,达到类似加密的效果,就暂时认为它是对代码进行了加密处理。

那说了这么多废话,社区有解决方案吗?我的回答是有的。但是这些方案在我踩过一遍坑之后,发现这些方案都有或多或少的问题,很难满足我的极致要求(即:使用官方脚手架实现全量代码加密要求)。

  • Electron 应用代码分为三层:main、preload 和 renderer,很多方案只做了一层(main)或两层(main 和 preload)处理,目前我并未发现做三层完全处理的示例或教程。

  • 社区实现自己的脚手架打包方案,魔改了很多官方通用配置。增大了迁移成本只是一方面,脱离官方很多问题的修复都要依赖于社区的积极性,在出现了和官方未对齐(新特性支持或者某些问题修复)的问题时,很难快速响应。甚至因为你使用了第三方打包方案,导致一些问题无解(丧失了官方的一些生态)。

说了这么多,那么社区到底有哪些方案呢?

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存